/*
 * WPA Supplicant - Layer2 packet handling with Microsoft NDISUIO
 * Copyright (c) 2003-2006, Jouni Malinen <j@w1.fi>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * Alternatively, this software may be distributed under the terms of BSD
 * license.
 *
 * See README and COPYING for more details.
 *
 * This implementation requires Windows specific event loop implementation,
 * i.e., eloop_win.c. In addition, the NDISUIO connection is shared with
 * driver_ndis.c, so only that driver interface can be used and
 * CONFIG_USE_NDISUIO must be defined.
 *
 * WinXP version of the code uses overlapped I/O and a single threaded design
 * with callback functions from I/O code. WinCE version uses a separate RX
 * thread that blocks on ReadFile() whenever the media status is connected.
 */

#include "includes.h"
#include <winsock2.h>
#include <ntddndis.h>

#ifdef _WIN32_WCE
#include <winioctl.h>
#include <nuiouser.h>
#endif /* _WIN32_WCE */

#include "common.h"
#include "eloop.h"
#include "l2_packet.h"

#ifndef _WIN32_WCE
/* from nuiouser.h */
#define FSCTL_NDISUIO_BASE      FILE_DEVICE_NETWORK
#define _NDISUIO_CTL_CODE(_Function, _Method, _Access) \
        CTL_CODE(FSCTL_NDISUIO_BASE, _Function, _Method, _Access)
#define IOCTL_NDISUIO_SET_ETHER_TYPE \
        _NDISUIO_CTL_CODE(0x202, METHOD_BUFFERED, \
                          FILE_READ_ACCESS | FILE_WRITE_ACCESS)
#endif /* _WIN32_WCE */

/* From driver_ndis.c to shared the handle to NDISUIO */
HANDLE driver_ndis_get_ndisuio_handle(void);

/*
 * NDISUIO supports filtering of only one ethertype at the time, so we must
 * fake support for two (EAPOL and RSN pre-auth) by switching to pre-auth
 * whenever wpa_supplicant is trying to pre-authenticate and then switching
 * back to EAPOL when pre-authentication has been completed.
 */

struct l2_packet_data;

struct l2_packet_ndisuio_global {
        int refcount;
        unsigned short first_proto;
        struct l2_packet_data *l2[2];
#ifdef _WIN32_WCE
        HANDLE rx_thread;
        HANDLE stop_request;
        HANDLE ready_for_read;
        HANDLE rx_processed;
#endif /* _WIN32_WCE */
};

static struct l2_packet_ndisuio_global *l2_ndisuio_global = NULL;

struct l2_packet_data {
        char ifname[100];
        u8 own_addr[ETH_ALEN];
        void (*rx_callback)(void *ctx, const u8 *src_addr,
                            const u8 *buf, size_t len);
        void *rx_callback_ctx;
        int l2_hdr; /* whether to include layer 2 (Ethernet) header in calls to
                     * rx_callback and l2_packet_send() */
        HANDLE rx_avail;
#ifndef _WIN32_WCE
        OVERLAPPED rx_overlapped;
#endif /* _WIN32_WCE */
        u8 rx_buf[1514];
        DWORD rx_written;
};


int l2_packet_get_own_addr(struct l2_packet_data *l2, u8 *addr)
{
        os_memcpy(addr, l2->own_addr, ETH_ALEN);
        return 0;
}


int l2_packet_send(struct l2_packet_data *l2, const u8 *dst_addr, u16 proto,
                   const u8 *buf, size_t len)
{
        BOOL res;
        DWORD written;
        struct l2_ethhdr *eth;
#ifndef _WIN32_WCE
        OVERLAPPED overlapped;
#endif /* _WIN32_WCE */
        OVERLAPPED *o;

        if (l2 == NULL)
                return -1;

#ifdef _WIN32_WCE
        o = NULL;
#else /* _WIN32_WCE */
        os_memset(&overlapped, 0, sizeof(overlapped));
        o = &overlapped;
#endif /* _WIN32_WCE */

        if (l2->l2_hdr) {
                res = WriteFile(driver_ndis_get_ndisuio_handle(), buf, len,
                                &written, o);
        } else {
                size_t mlen = sizeof(*eth) + len;
                eth = os_malloc(mlen);
                if (eth == NULL)
                        return -1;

                os_memcpy(eth->h_dest, dst_addr, ETH_ALEN);
                os_memcpy(eth->h_source, l2->own_addr, ETH_ALEN);
                eth->h_proto = htons(proto);
                os_memcpy(eth + 1, buf, len);
                res = WriteFile(driver_ndis_get_ndisuio_handle(), eth, mlen,
                                &written, o);
                os_free(eth);
        }

        if (!res) {
                DWORD err = GetLastError();
#ifndef _WIN32_WCE
                if (err == ERROR_IO_PENDING) {
                        /* For now, just assume that the packet will be sent in
                         * time before the next write happens. This could be
                         * cleaned up at some point to actually wait for
                         * completion before starting new writes.
                         */
                        return 0;
                }
#endif /* _WIN32_WCE */
                wpa_printf(MSG_DEBUG, "L2(NDISUIO): WriteFile failed: %d",
                           (int) GetLastError());
                return -1;
        }

        return 0;
}


static void l2_packet_callback(struct l2_packet_data *l2);

#ifdef _WIN32_WCE
static void l2_packet_rx_thread_try_read(struct l2_packet_data *l2)
{
        HANDLE handles[2];

        wpa_printf(MSG_MSGDUMP, "l2_packet_rx_thread: -> ReadFile");
        if (!ReadFile(driver_ndis_get_ndisuio_handle(), l2->rx_buf,
                      sizeof(l2->rx_buf), &l2->rx_written, NULL)) {
                DWORD err = GetLastError();
                wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: ReadFile failed: "
                           "%d", (int) err);
                /*
                 * ReadFile on NDISUIO/WinCE returns ERROR_DEVICE_NOT_CONNECTED
                 * error whenever the connection is not up. Yield the thread to
                 * avoid triggering a busy loop. Connection event should stop
                 * us from looping for long, but we need to allow enough CPU
                 * for the main thread to process the media disconnection.
                 */
                Sleep(100);
                return;
        }

        wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: Read %d byte packet",
                   (int) l2->rx_written);

        /*
         * Notify the main thread about the availability of a frame and wait
         * for the frame to be processed.
         */
        SetEvent(l2->rx_avail);
        handles[0] = l2_ndisuio_global->stop_request;
        handles[1] = l2_ndisuio_global->rx_processed;
        WaitForMultipleObjects(2, handles, FALSE, INFINITE);
        ResetEvent(l2_ndisuio_global->rx_processed);
}


static DWORD WINAPI l2_packet_rx_thread(LPVOID arg)
{
        struct l2_packet_data *l2 = arg;
        DWORD res;
        HANDLE handles[2];
        int run = 1;

        wpa_printf(MSG_DEBUG, "L2(NDISUIO): RX thread started");
        handles[0] = l2_ndisuio_global->stop_request;
        handles[1] = l2_ndisuio_global->ready_for_read;

        /*
         * Unfortunately, NDISUIO on WinCE does not seem to support waiting
         * on the handle. There do not seem to be anything else that we could
         * wait for either. If one were to modify NDISUIO to set a named event
         * whenever packets are available, this event could be used here to
         * avoid having to poll for new packets or we could even move to use a
         * single threaded design.
         *
         * In addition, NDISUIO on WinCE is returning
         * ERROR_DEVICE_NOT_CONNECTED whenever ReadFile() is attempted while
         * the adapter is not in connected state. For now, we are just using a
         * local event to allow ReadFile calls only after having received NDIS
         * media connect event. This event could be easily converted to handle
         * another event if the protocol driver is replaced with somewhat more
         * useful design.
         */

        while (l2_ndisuio_global && run) {
                res = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
                switch (res) {
                case WAIT_OBJECT_0:
                        wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: Received "
                                   "request to stop RX thread");
                        run = 0;
                        break;
                case WAIT_OBJECT_0 + 1:
                        l2_packet_rx_thread_try_read(l2);
                        break;
                case WAIT_FAILED:
                default:
                        wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: "
                                   "WaitForMultipleObjects failed: %d",
                                   (int) GetLastError());
                        run = 0;
                        break;
                }
        }

        wpa_printf(MSG_DEBUG, "L2(NDISUIO): RX thread stopped");

        return 0;
}
#else /* _WIN32_WCE */
static int l2_ndisuio_start_read(struct l2_packet_data *l2, int recursive)
{
        os_memset(&l2->rx_overlapped, 0, sizeof(l2->rx_overlapped));
        l2->rx_overlapped.hEvent = l2->rx_avail;
        if (!ReadFile(driver_ndis_get_ndisuio_handle(), l2->rx_buf,
                      sizeof(l2->rx_buf), &l2->rx_written, &l2->rx_overlapped))
        {
                DWORD err = GetLastError();
                if (err != ERROR_IO_PENDING) {
                        wpa_printf(MSG_DEBUG, "L2(NDISUIO): ReadFile failed: "
                                   "%d", (int) err);
                        return -1;
                }
                /*
                 * Once read is completed, l2_packet_rx_event() will be
                 * called.
                 */
        } else {
                wpa_printf(MSG_DEBUG, "L2(NDISUIO): ReadFile returned data "
                           "without wait for completion");
                if (!recursive)
                        l2_packet_callback(l2);
        }

        return 0;
}
#endif /* _WIN32_WCE */


static void l2_packet_callback(struct l2_packet_data *l2)
{
        const u8 *rx_buf, *rx_src;
        size_t rx_len;
        struct l2_ethhdr *ethhdr = (struct l2_ethhdr *) l2->rx_buf;

        wpa_printf(MSG_DEBUG, "L2(NDISUIO): Read %d bytes",
                   (int) l2->rx_written);

        if (l2->l2_hdr || l2->rx_written < sizeof(*ethhdr)) {
                rx_buf = (u8 *) ethhdr;
                rx_len = l2->rx_written;
        } else {
                rx_buf = (u8 *) (ethhdr + 1);
                rx_len = l2->rx_written - sizeof(*ethhdr);
        }
        rx_src = ethhdr->h_source;

        l2->rx_callback(l2->rx_callback_ctx, rx_src, rx_buf, rx_len);
#ifndef _WIN32_WCE
        l2_ndisuio_start_read(l2, 1);
#endif /* _WIN32_WCE */
}


static void l2_packet_rx_event(void *eloop_data, void *user_data)
{
        struct l2_packet_data *l2 = eloop_data;

        if (l2_ndisuio_global)
                l2 = l2_ndisuio_global->l2[l2_ndisuio_global->refcount - 1];

        ResetEvent(l2->rx_avail);

#ifndef _WIN32_WCE
        if (!GetOverlappedResult(driver_ndis_get_ndisuio_handle(),
                                 &l2->rx_overlapped, &l2->rx_written, FALSE)) {
                wpa_printf(MSG_DEBUG, "L2(NDISUIO): GetOverlappedResult "
                           "failed: %d", (int) GetLastError());
                return;
        }
#endif /* _WIN32_WCE */

        l2_packet_callback(l2);

#ifdef _WIN32_WCE
        SetEvent(l2_ndisuio_global->rx_processed);
#endif /* _WIN32_WCE */
}


static int l2_ndisuio_set_ether_type(unsigned short protocol)
{
        USHORT proto = htons(protocol);
        DWORD written;

        if (!DeviceIoControl(driver_ndis_get_ndisuio_handle(),
                             IOCTL_NDISUIO_SET_ETHER_TYPE, &proto,
                             sizeof(proto), NULL, 0, &written, NULL)) {
                wpa_printf(MSG_ERROR, "L2(NDISUIO): "
                           "IOCTL_NDISUIO_SET_ETHER_TYPE failed: %d",
                           (int) GetLastError());
                return -1;
        }

        return 0;
}


struct l2_packet_data * l2_packet_init(
        const char *ifname, const u8 *own_addr, unsigned short protocol,
        void (*rx_callback)(void *ctx, const u8 *src_addr,
                            const u8 *buf, size_t len),
        void *rx_callback_ctx, int l2_hdr)
{
        struct l2_packet_data *l2;

        if (l2_ndisuio_global == NULL) {
                l2_ndisuio_global = os_zalloc(sizeof(*l2_ndisuio_global));
                if (l2_ndisuio_global == NULL)
                        return NULL;
                l2_ndisuio_global->first_proto = protocol;
        }
        if (l2_ndisuio_global->refcount >= 2) {
                wpa_printf(MSG_ERROR, "L2(NDISUIO): Not more than two "
                           "simultaneous connections allowed");
                return NULL;
        }
        l2_ndisuio_global->refcount++;

        l2 = os_zalloc(sizeof(struct l2_packet_data));
        if (l2 == NULL)
                return NULL;
        l2_ndisuio_global->l2[l2_ndisuio_global->refcount - 1] = l2;

        os_strlcpy(l2->ifname, ifname, sizeof(l2->ifname));
        l2->rx_callback = rx_callback;
        l2->rx_callback_ctx = rx_callback_ctx;
        l2->l2_hdr = l2_hdr;

        if (own_addr)
                os_memcpy(l2->own_addr, own_addr, ETH_ALEN);

        if (l2_ndisuio_set_ether_type(protocol) < 0) {
                os_free(l2);
                return NULL;
        }

        if (l2_ndisuio_global->refcount > 1) {
                wpa_printf(MSG_DEBUG, "L2(NDISUIO): Temporarily setting "
                           "filtering ethertype to %04x", protocol);
                if (l2_ndisuio_global->l2[0])
                        l2->rx_avail = l2_ndisuio_global->l2[0]->rx_avail;
                return l2;
        }

        l2->rx_avail = CreateEvent(NULL, TRUE, FALSE, NULL);
        if (l2->rx_avail == NULL) {
                os_free(l2);
                return NULL;
        }

        eloop_register_event(l2->rx_avail, sizeof(l2->rx_avail),
                             l2_packet_rx_event, l2, NULL);

#ifdef _WIN32_WCE
        l2_ndisuio_global->stop_request = CreateEvent(NULL, TRUE, FALSE, NULL);
        /*
         * This event is being set based on media connect/disconnect
         * notifications in driver_ndis.c.
         */
        l2_ndisuio_global->ready_for_read =
                CreateEvent(NULL, TRUE, FALSE, TEXT("WpaSupplicantConnected"));
        l2_ndisuio_global->rx_processed = CreateEvent(NULL, TRUE, FALSE, NULL);
        if (l2_ndisuio_global->stop_request == NULL ||
            l2_ndisuio_global->ready_for_read == NULL ||
            l2_ndisuio_global->rx_processed == NULL) {
                if (l2_ndisuio_global->stop_request) {
                        CloseHandle(l2_ndisuio_global->stop_request);
                        l2_ndisuio_global->stop_request = NULL;
                }
                if (l2_ndisuio_global->ready_for_read) {
                        CloseHandle(l2_ndisuio_global->ready_for_read);
                        l2_ndisuio_global->ready_for_read = NULL;
                }
                if (l2_ndisuio_global->rx_processed) {
                        CloseHandle(l2_ndisuio_global->rx_processed);
                        l2_ndisuio_global->rx_processed = NULL;
                }
                eloop_unregister_event(l2->rx_avail, sizeof(l2->rx_avail));
                os_free(l2);
                return NULL;
        }

        l2_ndisuio_global->rx_thread = CreateThread(NULL, 0,
                                                    l2_packet_rx_thread, l2, 0,
                                                    NULL);
        if (l2_ndisuio_global->rx_thread == NULL) {
                wpa_printf(MSG_INFO, "L2(NDISUIO): Failed to create RX "
                           "thread: %d", (int) GetLastError());
                eloop_unregister_event(l2->rx_avail, sizeof(l2->rx_avail));
                CloseHandle(l2_ndisuio_global->stop_request);
                l2_ndisuio_global->stop_request = NULL;
                os_free(l2);
                return NULL;
        }
#else /* _WIN32_WCE */
        l2_ndisuio_start_read(l2, 0);
#endif /* _WIN32_WCE */

        return l2;
}


void l2_packet_deinit(struct l2_packet_data *l2)
{
        if (l2 == NULL)
                return;

        if (l2_ndisuio_global) {
                l2_ndisuio_global->refcount--;
                l2_ndisuio_global->l2[l2_ndisuio_global->refcount] = NULL;
                if (l2_ndisuio_global->refcount) {
                        wpa_printf(MSG_DEBUG, "L2(NDISUIO): restore filtering "
                                   "ethertype to %04x",
                                   l2_ndisuio_global->first_proto);
                        l2_ndisuio_set_ether_type(
                                l2_ndisuio_global->first_proto);
                        return;
                }

#ifdef _WIN32_WCE
                wpa_printf(MSG_DEBUG, "L2(NDISUIO): Waiting for RX thread to "
                           "stop");
                SetEvent(l2_ndisuio_global->stop_request);
                /*
                 * Cancel pending ReadFile() in the RX thread (if we were still
                 * connected at this point).
                 */
                if (!DeviceIoControl(driver_ndis_get_ndisuio_handle(),
                                     IOCTL_CANCEL_READ, NULL, 0, NULL, 0, NULL,
                                     NULL)) {
                        wpa_printf(MSG_DEBUG, "L2(NDISUIO): IOCTL_CANCEL_READ "
                                   "failed: %d", (int) GetLastError());
                        /* RX thread will exit blocking ReadFile once NDISUIO
                         * notices that the adapter is disconnected. */
                }
                WaitForSingleObject(l2_ndisuio_global->rx_thread, INFINITE);
                wpa_printf(MSG_DEBUG, "L2(NDISUIO): RX thread exited");
                CloseHandle(l2_ndisuio_global->rx_thread);
                CloseHandle(l2_ndisuio_global->stop_request);
                CloseHandle(l2_ndisuio_global->ready_for_read);
                CloseHandle(l2_ndisuio_global->rx_processed);
#endif /* _WIN32_WCE */

                os_free(l2_ndisuio_global);
                l2_ndisuio_global = NULL;
        }

#ifndef _WIN32_WCE
        CancelIo(driver_ndis_get_ndisuio_handle());
#endif /* _WIN32_WCE */

        eloop_unregister_event(l2->rx_avail, sizeof(l2->rx_avail));
        CloseHandle(l2->rx_avail);
        os_free(l2);
}


int l2_packet_get_ip_addr(struct l2_packet_data *l2, char *buf, size_t len)
{
        return -1;
}


void l2_packet_notify_auth_start(struct l2_packet_data *l2)
{
}
